using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Windows;
using System.Windows.Data;
using Gko.Utils;
using Gko.Wpf.Mvvm.Interfaces;

namespace Gko.Wpf.Mvvm
{
	public abstract class ViewFactoryBase : IViewFactory
	{
		private readonly Dictionary<Type, Type> viewModelToView;

		protected ViewFactoryBase()
		{
			this.viewModelToView = new Dictionary<Type, Type>();
		}

		#region IViewFactory Members

		/// <exception cref="InvalidOperationException"><c>InvalidOperationException</c>.</exception>
		public FrameworkElement CreateView<TViewModel>(TViewModel viewModel) where TViewModel : class, IViewModel
		{
			if (viewModel == null) {
				throw new ArgumentNullException("viewModel");
			}
			var viewModelType = typeof(TViewModel);
			Type viewType;
			if (!this.viewModelToView.TryGetValue(viewModelType, out viewType)) {
				viewModelType = viewModel.GetType();
				if (!this.viewModelToView.TryGetValue(viewModelType, out viewType)) {
					throw new InvalidOperationException("Type of view is not registered for " + viewModelType.FullName);
				}
			}
			var view = (FrameworkElement)Activator.CreateInstance(viewType, this.GetViewConstructorArguments(viewType).ToArray());
			this.InitializeView(view, viewModel);
			return view;
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")]
		public void AttachViewModelToView<TViewModel, TView>()
			where TViewModel : class, IViewModel
			where TView : FrameworkElement
		{
			var viewType = typeof(TView);
			var viewModelType = typeof(TViewModel);
			this.viewModelToView[viewModelType] = viewType;
			if (typeof(Window).IsAssignableFrom(viewType)) {
				return;
			}


			var visualTree = new FrameworkElementFactory(viewType);
			visualTree.SetBinding(FrameworkElement.DataContextProperty, new Binding());
			const BindingFlags Flags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static;

			foreach (var method in viewModelType.GetMethods(Flags)) {
				foreach (var attribute in AttributeUtils.GetAttributes<SubscribeToEventAttribute>(method, true)) {
					var handler = CreateHandler(method, attribute.Event.HandlerType);
					visualTree.AddHandler(attribute.Event, handler, true);
				}
			}

			var resource = new DataTemplate(viewModelType)
			{
				DataType = viewModelType,
				VisualTree = visualTree,
			};
			Application.Current.Resources.Add(new DataTemplateKey(viewModelType), resource);
		}

		public virtual void InitializeView<TViewModel>(FrameworkElement view, TViewModel viewModel)
			where TViewModel : class, IViewModel
		{
			view.DataContext = viewModel;
			var viewModelType = viewModel.GetType();
			const BindingFlags Flags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static;

			foreach (var method in viewModelType.GetMethods(Flags)) {
				foreach (var attribute in AttributeUtils.GetAttributes<SubscribeToEventAttribute>(method, true)) {
					var handler = CreateHandler(method, attribute.Event.HandlerType);
					view.AddHandler(attribute.Event, handler, true);
				}
			}
		}

		#endregion

		protected virtual IEnumerable<object> GetViewConstructorArguments(Type viewType)
		{
			yield break;
		}

		private static Delegate CreateHandler(MethodInfo method, Type handlerType)
		{
			const BindingFlags Flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
			var types = Type.EmptyTypes;
			foreach (var m in handlerType.GetMethods(Flags)) {
				if (m.Name == "Invoke") {
					types = m.GetParameters().Select(p => p.ParameterType).ToArray();
				}
			}
			var getHandlerMethodInfo = typeof(ViewFactoryBase).GetMethod("GetEventHandler", BindingFlags.Static | BindingFlags.NonPublic);
			getHandlerMethodInfo = getHandlerMethodInfo.MakeGenericMethod(types[1]);
			var result = (Delegate)getHandlerMethodInfo.Invoke(null, new[] { method });
			return DelegateUtils.Convert(handlerType, result);
		}

		private static Action<object, TEventArgs> GetEventHandler<TEventArgs>(MethodInfo method)
			where TEventArgs : RoutedEventArgs
		{
			return FunctionalExtensions.Partial<MethodInfo, object, TEventArgs>(OnEvent, method);
		}

		private static void OnEvent<TEventArgs>(MethodInfo method, object sender, TEventArgs args)
			where TEventArgs : RoutedEventArgs
		{
			var view = (FrameworkElement)sender;
			var viewModel = (IViewModel)view.DataContext;
			var parameterInfos = method.GetParameters();
			object[] parameters = null;
			switch ( parameterInfos.Length ) {
				case 0:
					break;
				case 1:
					if( parameterInfos[0].ParameterType == typeof( object ) ) {
						parameters = sender.ToSequence().ToArray();
					} else if( parameterInfos[0].ParameterType.IsAssignableFrom( args.GetType() ) ) {
						parameters = args.ToSequence().ToArray();
					} else {
						throw new InvalidOperationException("Can not invoke event handler. Invalid argument type.");
					}
					break;
				default:
					parameters = sender.ToSequence(args).ToArray();
					break;
			}
			method.Invoke( viewModel, parameters );
		}
	}
}